/*
    SPDX-FileCopyrightText: 2008 Will Stephenson <wstephenson@kde.org>
    SPDX-FileCopyrightText: 2011-2012 Rajeesh K Nambiar <rajeeshknambiar@gmail.com>
    SPDX-FileCopyrightText: 2011-2012 Lamarque V. Souza <lamarque@kde.org>
    SPDX-FileCopyrightText: 2013 Lukas Tinkl <ltinkl@redhat.com>

    SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/

#include "vpnc.h"
#include "plasma_nm_vpnc.h"

#include <QFile>
#include <QFileInfo>
#include <QStandardPaths>

#include "nm-vpnc-service.h"
#include <KLocalizedString>
#include <KMessageBox>
#include <KPluginFactory>
#include <KSharedConfig>

#include <NetworkManagerQt/Connection>
#include <NetworkManagerQt/Ipv4Setting>
#include <NetworkManagerQt/VpnSetting>

#include <arpa/inet.h>

#include "vpncauth.h"
#include "vpncwidget.h"

VpncUiPluginPrivate::VpncUiPluginPrivate()
{
    decryptedPasswd.clear();
    ciscoDecrypt = nullptr;
}

VpncUiPluginPrivate::~VpncUiPluginPrivate() = default;

QString VpncUiPluginPrivate::readStringKeyValue(const KConfigGroup &configGroup, const QString &key)
{
    const QString retValue = configGroup.readEntry(key);
    if (retValue.isEmpty()) {
        // String key can also start with "!" in CISCO pcf file.
        return configGroup.readEntry('!' + key);
    } else {
        return retValue;
    }
}

void VpncUiPluginPrivate::gotCiscoDecryptOutput()
{
    QByteArray output = ciscoDecrypt->readAll();
    if (!output.isEmpty()) {
        const QList<QByteArray> lines = output.split('\n');
        if (!lines.isEmpty()) {
            decryptedPasswd = QString::fromUtf8(lines.first());
        }
    }
}

void VpncUiPluginPrivate::ciscoDecryptFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
    if (exitCode || exitStatus != QProcess::NormalExit) {
        decryptedPasswd.clear();
    }
}

void VpncUiPluginPrivate::ciscoDecryptError(QProcess::ProcessError pError)
{
    if (!pError) {
        qCWarning(PLASMA_NM_VPNC_LOG) << "Error in executing cisco-decrypt";
        KMessageBox::error(nullptr, i18n("Error decrypting the obfuscated password"), i18n("Error"), KMessageBox::Notify);
    }
    decryptedPasswd.clear();
}

#define NM_VPNC_LOCAL_PORT_DEFAULT 500

K_PLUGIN_CLASS_WITH_JSON(VpncUiPlugin, "plasmanetworkmanagement_vpncui.json")

VpncUiPlugin::VpncUiPlugin(QObject *parent, const QVariantList &)
    : VpnUiPlugin(parent)
{
}

VpncUiPlugin::~VpncUiPlugin() = default;

SettingWidget *VpncUiPlugin::widget(const NetworkManager::VpnSetting::Ptr &setting, QWidget *parent)
{
    return new VpncWidget(setting, parent);
}

SettingWidget *VpncUiPlugin::askUser(const NetworkManager::VpnSetting::Ptr &setting, const QStringList &hints, QWidget *parent)
{
    return new VpncAuthDialog(setting, hints, parent);
}

QString VpncUiPlugin::suggestedFileName(const NetworkManager::ConnectionSettings::Ptr &connection) const
{
    return connection->id() + ".pcf";
}

QString VpncUiPlugin::supportedFileExtensions() const
{
    return "*.pcf";
}

NMVariantMapMap VpncUiPlugin::importConnectionSettings(const QString &fileName)
{
    // qCDebug(PLASMA_NM_VPNC_LOG) << "Importing Cisco VPN connection from " << fileName;

    VpncUiPluginPrivate *decrPlugin = nullptr;
    NMVariantMapMap result;

    if (!fileName.endsWith(QLatin1String(".pcf"), Qt::CaseInsensitive)) {
        return result;
    }

    mError = VpnUiPlugin::Error;

    // NOTE: Cisco VPN pcf files follow ini style matching KConfig files
    // http://www.cisco.com/en/US/docs/security/vpn_client/cisco_vpn_client/vpn_client46/administration/guide/vcAch2.html#wp1155033
    KSharedConfig::Ptr config = KSharedConfig::openConfig(fileName);
    if (!config) {
        mErrorMessage = i18n("File %1 could not be opened.", fileName);
        return result;
    }

    KConfigGroup cg(config, "main"); // Keys&Values are stored under [main]
    if (cg.exists()) {
        // Setup cisco-decrypt binary to decrypt the passwords
        const QString ciscoDecryptBinary = QStandardPaths::findExecutable("cisco-decrypt");
        if (ciscoDecryptBinary.isEmpty()) {
            mErrorMessage = i18n("Needed executable cisco-decrypt could not be found.");
            return result;
        }

        decrPlugin = new VpncUiPluginPrivate();
        decrPlugin->ciscoDecrypt = new KProcess(decrPlugin);
        decrPlugin->ciscoDecrypt->setOutputChannelMode(KProcess::OnlyStdoutChannel);
        decrPlugin->ciscoDecrypt->setReadChannel(QProcess::StandardOutput);
        connect(decrPlugin->ciscoDecrypt, &KProcess::errorOccurred, decrPlugin, &VpncUiPluginPrivate::ciscoDecryptError);
        connect(decrPlugin->ciscoDecrypt,
                QOverload<int, QProcess::ExitStatus>::of(&KProcess::finished),
                decrPlugin,
                &VpncUiPluginPrivate::ciscoDecryptFinished);
        connect(decrPlugin->ciscoDecrypt, &KProcess::readyReadStandardOutput, decrPlugin, &VpncUiPluginPrivate::gotCiscoDecryptOutput);

        NMStringMap data;
        NMStringMap secretData;
        QVariantMap ipv4Data;

        // gateway
        data.insert(NM_VPNC_KEY_GATEWAY, decrPlugin->readStringKeyValue(cg, "Host"));
        // group name
        data.insert(NM_VPNC_KEY_ID, decrPlugin->readStringKeyValue(cg, "GroupName"));
        // user password
        if (!decrPlugin->readStringKeyValue(cg, "UserPassword").isEmpty()) {
            secretData.insert(NM_VPNC_KEY_XAUTH_PASSWORD, decrPlugin->readStringKeyValue(cg, "UserPassword"));
        } else if (!decrPlugin->readStringKeyValue(cg, "enc_UserPassword").isEmpty() && !ciscoDecryptBinary.isEmpty()) {
            // Decrypt the password and insert into map
            decrPlugin->ciscoDecrypt->setProgram(ciscoDecryptBinary);
            decrPlugin->ciscoDecrypt->start();
            decrPlugin->ciscoDecrypt->waitForStarted();
            decrPlugin->ciscoDecrypt->write(decrPlugin->readStringKeyValue(cg, "enc_UserPassword").toUtf8());
            if (decrPlugin->ciscoDecrypt->waitForFinished()) {
                secretData.insert(NM_VPNC_KEY_XAUTH_PASSWORD, decrPlugin->decryptedPasswd);
            }
        }
        // Save user password
        switch (cg.readEntry("SaveUserPassword").toInt()) {
        case 0:
            data.insert(NM_VPNC_KEY_XAUTH_PASSWORD "-flags", QString::number(NetworkManager::Setting::NotSaved));
            break;
        case 1:
            data.insert(NM_VPNC_KEY_XAUTH_PASSWORD "-flags", QString::number(NetworkManager::Setting::AgentOwned));
            break;
        case 2:
            data.insert(NM_VPNC_KEY_XAUTH_PASSWORD "-flags", QString::number(NetworkManager::Setting::NotRequired));
            break;
        }

        // group password
        if (!decrPlugin->readStringKeyValue(cg, "GroupPwd").isEmpty()) {
            secretData.insert(NM_VPNC_KEY_SECRET, decrPlugin->readStringKeyValue(cg, "GroupPwd"));
            data.insert(NM_VPNC_KEY_SECRET "-flags", QString::number(NetworkManager::Setting::AgentOwned));
        } else if (!decrPlugin->readStringKeyValue(cg, "enc_GroupPwd").isEmpty() && !ciscoDecryptBinary.isEmpty()) {
            // Decrypt the password and insert into map
            decrPlugin->ciscoDecrypt->setProgram(ciscoDecryptBinary);
            decrPlugin->ciscoDecrypt->start();
            decrPlugin->ciscoDecrypt->waitForStarted();
            decrPlugin->ciscoDecrypt->write(decrPlugin->readStringKeyValue(cg, "enc_GroupPwd").toUtf8());
            if (decrPlugin->ciscoDecrypt->waitForFinished()) {
                secretData.insert(NM_VPNC_KEY_SECRET, decrPlugin->decryptedPasswd);
                data.insert(NM_VPNC_KEY_SECRET "-flags", QString::number(NetworkManager::Setting::AgentOwned));
            }
        }

        // Auth Type
        if (!cg.readEntry("AuthType").isEmpty() && cg.readEntry("AuthType").toInt() == 5) {
            data.insert(NM_VPNC_KEY_AUTHMODE, QLatin1String("hybrid"));
        }

        // Optional settings
        // username
        if (!decrPlugin->readStringKeyValue(cg, "Username").isEmpty()) {
            data.insert(NM_VPNC_KEY_XAUTH_USER, decrPlugin->readStringKeyValue(cg, "Username"));
        }
        // domain
        if (!decrPlugin->readStringKeyValue(cg, "NTDomain").isEmpty()) {
            data.insert(NM_VPNC_KEY_DOMAIN, decrPlugin->readStringKeyValue(cg, "NTDomain"));
        }
        // encryption
        if (!cg.readEntry("SingleDES").isEmpty() && cg.readEntry("SingleDES").toInt() != 0) {
            data.insert(NM_VPNC_KEY_SINGLE_DES, QLatin1String("yes"));
        }
        /* Disable all NAT Traversal if explicit EnableNat=0 exists, otherwise
         * default to NAT-T which is newer and standardized.  If EnableNat=1, then
         * use Cisco-UDP like always; but if the key "X-NM-Use-NAT-T" is set, then
         * use NAT-T.  If the key "X-NM-Force-NAT-T" is set then force NAT-T always
         * on.  See vpnc documentation for more information on what the different
         * NAT modes are.
         */
        // enable NAT
        if (cg.readEntry("EnableNat").toInt() == 1) {
            data.insert(NM_VPNC_KEY_NAT_TRAVERSAL_MODE, QLatin1String(NM_VPNC_NATT_MODE_CISCO));
            // NAT traversal
            if (!cg.readEntry("X-NM-Use-NAT-T").isEmpty()) {
                if (cg.readEntry("X-NM-Use-NAT-T").toInt() == 1) {
                    data.insert(NM_VPNC_KEY_NAT_TRAVERSAL_MODE, QLatin1String(NM_VPNC_NATT_MODE_NATT));
                }
                if (cg.readEntry("X-NM-Force-NAT-T").toInt() == 1) {
                    data.insert(NM_VPNC_KEY_NAT_TRAVERSAL_MODE, QLatin1String(NM_VPNC_NATT_MODE_NATT_ALWAYS));
                }
            }
        } else {
            data.insert(NM_VPNC_KEY_NAT_TRAVERSAL_MODE, QLatin1String(NM_VPNC_NATT_MODE_NONE));
        }
        // dead peer detection
        data.insert(NM_VPNC_KEY_DPD_IDLE_TIMEOUT, cg.readEntry("PeerTimeout"));
        // UseLegacyIKEPort=0 uses dynamic source IKE port instead of 500.
        if (cg.readEntry("UseLegacyIKEPort").isEmpty() || cg.readEntry("UseLegacyIKEPort").toInt() != 0) {
            data.insert(NM_VPNC_KEY_LOCAL_PORT, QString::number(NM_VPNC_LOCAL_PORT_DEFAULT));
        }
        // DH Group
        data.insert(NM_VPNC_KEY_DHGROUP, decrPlugin->readStringKeyValue(cg, "DHGroup"));
        // Tunneling Mode - not supported by vpnc
        if (cg.readEntry("TunnelingMode").toInt() == 1) {
            KMessageBox::error(
                nullptr,
                i18n("The VPN settings file '%1' specifies that VPN traffic should be tunneled through TCP which is currently not supported in the vpnc "
                     "software.\n\nThe connection can still be created, with TCP tunneling disabled, however it may not work as expected.",
                     fileName),
                i18n("Not supported"),
                KMessageBox::Notify);
        }
        // EnableLocalLAN and X-NM-Routes are to be added to IPv4Setting
        if (!cg.readEntry("EnableLocalLAN").isEmpty()) {
            ipv4Data.insert("never-default", cg.readEntry("EnableLocalLAN"));
        }
        if (!decrPlugin->readStringKeyValue(cg, "X-NM-Routes").isEmpty()) {
            QList<NetworkManager::IpRoute> list;
            for (const QString &route : decrPlugin->readStringKeyValue(cg, "X-NM-Routes").split(' ')) {
                NetworkManager::IpRoute ipRoute;
                ipRoute.setIp(QHostAddress(route.split(QLatin1Char('/')).first()));
                ipRoute.setPrefixLength(route.split(QLatin1Char('/')).at(1).toInt());
                list << ipRoute;
            }
            QList<QList<uint>> dbusRoutes;
            for (const NetworkManager::IpRoute &route : qAsConst(list)) {
                QList<uint> dbusRoute;
                dbusRoute << htonl(route.ip().toIPv4Address()) << route.prefixLength() << htonl(route.nextHop().toIPv4Address()) << route.metric();
                dbusRoutes << dbusRoute;
            }
            ipv4Data.insert("routes", QVariant::fromValue(dbusRoutes));
        }

        // Set the '...-type' and '...-flags' value also
        NetworkManager::VpnSetting setting;
        setting.setServiceType("org.freedesktop.NetworkManager.vpnc");
        setting.setData(data);
        setting.setSecrets(secretData);

        QVariantMap conn;
        if (decrPlugin->readStringKeyValue(cg, "Description").isEmpty()) {
            QFileInfo fileInfo(fileName);
            conn.insert("id", fileInfo.fileName().remove(QLatin1String(".pcf"), Qt::CaseInsensitive));
        } else {
            conn.insert("id", decrPlugin->readStringKeyValue(cg, "Description"));
        }
        conn.insert("type", "vpn");
        result.insert("connection", conn);

        result.insert("vpn", setting.toMap());

        if (!ipv4Data.isEmpty()) {
            result.insert("ipv4", ipv4Data);
        }

        delete decrPlugin;
    } else {
        mErrorMessage = i18n("%1: file format error.", fileName);
        return result;
    }

    mError = VpncUiPlugin::NoError;
    return result;
}

bool VpncUiPlugin::exportConnectionSettings(const NetworkManager::ConnectionSettings::Ptr &connection, const QString &fileName)
{
    NMStringMap data;
    NMStringMap secretData;

    NetworkManager::VpnSetting::Ptr vpnSetting = connection->setting(NetworkManager::Setting::Vpn).dynamicCast<NetworkManager::VpnSetting>();
    data = vpnSetting->data();
    secretData = vpnSetting->secrets();

    KSharedConfig::Ptr config = KSharedConfig::openConfig(fileName);
    if (!config) {
        mErrorMessage = i18n("%1: file could not be created", fileName);
        return false;
    }
    KConfigGroup cg(config, "main");

    cg.writeEntry("Description", connection->id());
    cg.writeEntry("Host", data.value(NM_VPNC_KEY_GATEWAY));
    if (data.value(NM_VPNC_KEY_AUTHMODE) == QLatin1String("hybrid")) {
        cg.writeEntry("AuthType", "5");
    } else {
        cg.writeEntry("AuthType", "1");
    }
    cg.writeEntry("GroupName", data.value(NM_VPNC_KEY_ID));
    cg.writeEntry("GroupPwd", secretData.value(NM_VPNC_KEY_SECRET));
    cg.writeEntry("UserPassword", secretData.value(NM_VPNC_KEY_XAUTH_PASSWORD));
    cg.writeEntry("enc_GroupPwd", "");
    cg.writeEntry("enc_UserPassword", "");
    if ((NetworkManager::Setting::SecretFlags)data.value(NM_VPNC_KEY_XAUTH_PASSWORD "-flags").toInt() & NetworkManager::Setting::NotSaved) {
        cg.writeEntry("SaveUserPassword", "0");
    }
    if ((NetworkManager::Setting::SecretFlags)data.value(NM_VPNC_KEY_XAUTH_PASSWORD "-flags").toInt() & NetworkManager::Setting::AgentOwned) {
        cg.writeEntry("SaveUserPassword", "1");
    }
    if ((NetworkManager::Setting::SecretFlags)data.value(NM_VPNC_KEY_XAUTH_PASSWORD "-flags").toInt() & NetworkManager::Setting::NotRequired) {
        cg.writeEntry("SaveUserPassword", "2");
    }
    cg.writeEntry("Username", data.value(NM_VPNC_KEY_XAUTH_USER));
    cg.writeEntry("EnableISPConnect", "0");
    cg.writeEntry("ISPConnectType", "0");
    cg.writeEntry("ISPConnect", "");
    cg.writeEntry("ISPCommand", "");
    cg.writeEntry("EnableBackup", "0");
    cg.writeEntry("BackupServer", "");
    cg.writeEntry("CertStore", "0");
    cg.writeEntry("CertName", "");
    cg.writeEntry("CertPath", "");
    cg.writeEntry("CertSubjectName", "");
    cg.writeEntry("CertSerialHash", "");
    cg.writeEntry("DHGroup", data.value(NM_VPNC_KEY_DHGROUP));
    cg.writeEntry("ForceKeepAlives", "0");
    cg.writeEntry("NTDomain", data.value(NM_VPNC_KEY_DOMAIN));
    cg.writeEntry("EnableMSLogon", "0");
    cg.writeEntry("MSLogonType", "0");
    cg.writeEntry("TunnelingMode", "0");
    cg.writeEntry("TcpTunnelingPort", "10000");
    cg.writeEntry("PeerTimeout", data.value(NM_VPNC_KEY_DPD_IDLE_TIMEOUT));
    cg.writeEntry("EnableLocalLAN", "1");
    cg.writeEntry("SendCertChain", "0");
    cg.writeEntry("VerifyCertDN", "");
    cg.writeEntry("EnableSplitDNS", "1");
    cg.writeEntry("SPPhonebook", "");
    if (data.value(NM_VPNC_KEY_SINGLE_DES) == "yes") {
        cg.writeEntry("SingleDES", "1");
    }
    if (data.value(NM_VPNC_KEY_NAT_TRAVERSAL_MODE) == NM_VPNC_NATT_MODE_CISCO) {
        cg.writeEntry("EnableNat", "1");
    }
    if (data.value(NM_VPNC_KEY_NAT_TRAVERSAL_MODE) == NM_VPNC_NATT_MODE_NATT) {
        cg.writeEntry("EnableNat", "1");
        cg.writeEntry("X-NM-Use-NAT-T", "1");
    }
    if (data.value(NM_VPNC_KEY_NAT_TRAVERSAL_MODE) == NM_VPNC_NATT_MODE_NATT_ALWAYS) {
        cg.writeEntry("EnableNat", "1");
        cg.writeEntry("X-NM-Force-NAT-T", "1");
    }
    // Export X-NM-Routes
    NetworkManager::Ipv4Setting::Ptr ipv4Setting = connection->setting(NetworkManager::Setting::Ipv4).dynamicCast<NetworkManager::Ipv4Setting>();
    if (!ipv4Setting->routes().isEmpty()) {
        QString routes;
        for (const NetworkManager::IpRoute &route : ipv4Setting->routes()) {
            routes += route.ip().toString() + QLatin1Char('/') + QString::number(route.prefixLength()) + QLatin1Char(' ');
        }
        cg.writeEntry("X-NM-Routes", routes.trimmed());
    }

    cg.sync();

    mError = VpncUiPlugin::NoError;
    return true;
}

#include "vpnc.moc"
